// ###########################################################################################################################
// Establishes the basic setup and integration of the DreamSpace module.
// ###########################################################################################################################

module DreamSpace.System {
    // =======================================================================================================================

    export interface PropertyGetSetHandler<TDataType> { (value?: TDataType): TDataType; };

    export class StaticProperty<TDataType> extends PropertyCollection<TDataType> {
        // -------------------------------------------------------------------------------------------------------------------

        /** A reference to the type (constructor function object) that owns this static property. */
        owningType: IClassModule<Object>;

        /** A type place holder to use to apply expected property types for the TS compiler to enforce.
        * This instance property is never actually set, since it would only reference the static "Property" object itself, which is pointless. */
        propertyType: Property<Object, TDataType>;

        /** An internal name for the property.  This will also be the attribute set on the underlying UI element (so a name
        * of 'id' would set the 'id' attribute of the element).
        * WARNING: You cannot dynamically rename properties by changing this value, and doing so will most likely cause errors
        * since the name is a unique identifier for property references on graph item instances.
        */
        name: string;

        /** The default value for new related instance properties. */
        defaultValue: TDataType;

        /** If true (false by default), then 'onRedraw()' will be called when this property is updated. */
        isVisual: boolean;

        /** By default, properties only update internally and don't perform any special function outside the virtual environment.
        * To update an HTML element, or host property, use one of the predefined 'ClientPropertyHandlers' values, or add your own.
        */
        clientPropertyHandler: ClientPropertyHandler<GraphItem.$Type, TDataType>;

        /** A reference to the original get/set handler used for this static property.
        * This is for reference only, and changing it after the system starts up has no effect.
        */
        propertyGetSetHandler: PropertyGetSetHandler<TDataType>;

        constructor(owner: IClassModule<Object>, name: string, defaultValue?: TDataType, isVisual: boolean = false, clientPropertyHandler: ClientPropertyHandler<GraphItem.$Type, TDataType> = null) {
            super();
            this.owningType = owner;
            this.name = name;
            this.defaultValue = defaultValue;
            this.isVisual = isVisual;
            this.clientPropertyHandler = clientPropertyHandler;

            // ... register an accessor for this property as well on the prototype for the class (never on the instance!) ...
            // (note: developers don't have to expose the accessor (getter/setter), but if desired, they only have to declare the function signature)

            var _this = this;
            this.propertyGetSetHandler = owner.prototype["_" + name] = function (value?: TDataType): TDataType {
                if (arguments.length > 0) {
                    (<GraphItem.$Type>this).setValue(_this, value);
                }
                else return (<GraphItem.$Type>this).getValue(_this);
            };

            if (Compatibility.objectDefineProperty)
                Object.defineProperty(owner.prototype, name, {
                    configurable: true,
                    enumerable: true,
                    writable: true,
                    value: this.defaultValue,
                    get: function (): TDataType { return (<GraphItem.$Type>this).getValue(_this); },
                    set: function (v: TDataType) { (<GraphItem.$Type>this).setValue(_this, v); }
                });
        }

        // -------------------------------------------------------------------------------------------------------------------

        ///** Returns a default accessor (get/set) function to be used when creating accessor functions in GraphItem derived types.
        //* When 'GraphItem' is extended, the developer is able to create special "property functions" that get and set values in the internal
        //* 'properties' observable array (such as "x = obj.X()" for "get", and "obj.X(x)" for "set"+"get").
        //* Before a property accessor can be created however, you will first need to use 'Property.registerProperty()' to register a static
        //* property.  This allows for static based events, and registers the property as specific to a given class type.  You can think of 
        //* static properties as very similar to 'Dependency Properties' in XAML development (WPF/Silverlight).
        //* The 'registeredProperty' parameter is the static property that will be used when calling '{GraphItem}.setProperty()' and
        //* '{GraphItem}.getProperty()' (which ultimately updates the internal 'properties' array, allowing listeners to be notified).
        //* Typically, the properties used with accessors include HTML attributes specific to a particular element by default, but can
        //* also include custom attributes not part of the HTML element object as well, allowing for a very flexible UI model when
        //* custom properties and functionality is desired.
        //* Usage: somePropertyName = GraphItem.accessor<DataType>(someStaticProperty);
        //*/
        //accessor(): (value?: TDataType) => TDataType {
        //    var registeredProperty = this;
        //    return function (value?: TDataType): TDataType {
        //        if (arguments.length > 0)
        //            return (<GraphItem>this).setValue(registeredProperty, value);
        //        return (<GraphItem>this).getValue(registeredProperty);
        //    };
        //}

        // -------------------------------------------------------------------------------------------------------------------

        createPropertyInstance(owner: GraphItem.$Type, value?: any) {
            return new Property(owner, this, value === void 0 ? this.defaultValue : value);
        }

        // -------------------------------------------------------------------------------------------------------------------

        toString(): string { return this.name; }
        toLocaleString(): string { return this.name; }
        valueOf(): any { return this.name; }

        // -------------------------------------------------------------------------------------------------------------------
    }

    // =======================================================================================================================

    /** Represents a GraphItem instance property which holds a reference to the related static property information, and also stores the current instance value. */
    export class Property<TOwner extends GraphObject, TDataType> extends PropertyCollection<TDataType> {
        // -------------------------------------------------------------------------------------------------------------------

        /** Registers a list of GraphItem properties (usually UI attribute names) that, when updated, can update the UI, or trigger events.
        * You can think of static properties as very similar to 'Dependency Properties' in XAML development (WPF/Silverlight).
        * Usage (in class): someStaticProperty = Property.registerProperty(yourGraphItemDerivedType, "internalName", updateUI_TrueOrFalse);
        * (see 'DreamSpace.UI.ts' for examples)
        * @param {GraphItem} type Your derived 'GraphItem' type.
        *                         NOTE: This parameter expects a type who's constructor STARTS with an OPTIONAL parameter
        *                         (i.e. constructor(parent: GraphItem.$Type = null, ...)), which should be passed on to the super class.
        * @param {string} name The property name to set internally (this can be different from the static property name).
        *                      Note: The name you choose will also be set as an attribute on the element, so for example, using 'id' or 'name'
        *                      as a name will also update the underlying element's 'id' and 'name' attributes.
        * @param {TDataType} defaultValue The default value for all instance properties created from this static property.
        * @param {boolean} isVisual Set this to true on properties that affect the visual display of an element.  For example, some derived graph
        *                           items may contain text, size, or color type properties that, when updated, should be reflected on the UI.  This
        *                           is managed internally by automatically queuing an event to trigger once all scripts have completed.
        * @param {ClientPropertyHandler} clientPropertyHandler (optional) A callback to execute when the property has changed.
        * @param {Function} changedCallback (optional) A callback to execute when the property has changed.
        * @param {Function} changingCallback (optional) A callback to execute before the property changes.
        * @param {Function} filterCallback (optional) A callback to execute for value translation purposes when the property is read.
        * For example, changing "M"/"F" to "Male"/"Female", or a date from '20131006' to 'October 6th, 2013'.
        */
        static register<TOwner extends GraphItem.$Type, TDataType>(classModule: IClassModule<Object>, name: string, isVisual: boolean = false, defaultValue?: TDataType,
            clientPropertyHandler: ClientPropertyHandler<TOwner, TDataType> = null,
            changedCallback: ListenerCallback<TOwner, TDataType> = null,
            changingCallback: InterceptorCallback<TOwner, TDataType> = null,
            filterCallback: FilterCallback<TOwner, TDataType> = null)
            : StaticProperty<TDataType> {

            if (typeof classModule != 'function') throw Exception.from("A class type [function] is required for property registration.");
            if (name === void 0 || name === null || name === '') throw Exception.from("A name is required for property registration.");

            // ... get the current static property list for the type, or create a new one ...

            var currentSPList: __IStaticPropertyInfo<TDataType> = classModule.$__staticProperties;

            if (currentSPList === void 0) {
                classModule['$__staticProperties'] = currentSPList = <any>[];
                currentSPList.parent = null; // (this is the top most base class type, so there's no parent static property list)
                currentSPList.owner = classModule; // (the base class type)
                currentSPList.namedIndex = {};
            }

            // ... make sure the name is not already taken! ...

            if (currentSPList.namedIndex[name])
                throw Exception.from("The specified static property name '" + name + "' was already used on this type (" + Object.getTypeName(classModule) + ").", classModule);

            // ... create the static property instance and add it ...

            var sProp = new StaticProperty<TDataType>(classModule, name, defaultValue, isVisual, clientPropertyHandler);

            if (changedCallback != null)
                sProp.registerListener(changedCallback);

            if (changingCallback != null)
                sProp.registerInterceptor(changingCallback);

            if (filterCallback)
                sProp.registerFilter(filterCallback);

            currentSPList.push(sProp);
            currentSPList.namedIndex[sProp.name] = sProp;

            return sProp;
        }

        static __completePropertyRegistration(classModule: IClassModule<Object>) {
            var moduleSPList = classModule.$__staticProperties;
            var classSPList = classModule.$Type.$__staticProperties;

            if (currentSPList.owner !== classModule) { // (if 'type['$__staticProperties']' returns a value, make sure the static array owner is of the same type, otherwise assume 'type' is a sub-class type)
                // (since TS's '__extends' function copies forward all the static properties to derived objects, a new array needs to be created at each inheritance level)
                var typeSpecificSP: typeof currentSPList = <any>[];
                typeSpecificSP.parent = currentSPList;
                typeSpecificSP.owner = classModule;
                typeSpecificSP.namedIndex = currentSPList.namedIndex; // (keep same instance for quick hash lookups)
                //??for (var i = 0, n = currentSP.length; i < n; ++i)
                //    typeSpecificSP.push(currentSP[i]);
                classModule['$__staticProperties'] = currentSPList = typeSpecificSP; // (set '$__staticProperties' on THIS SPECIFIC class type)
            }

            // ... make sure the name is not already taken! ...

            if (currentSPList.namedIndex[name])
                throw Exception.from("The specified static property name '" + name + "' was already used on this type (" + Object.getTypeName(classModule) + ").", classModule);
        }

        // -------------------------------------------------------------------------------------------------------------------

        /** The 'GraphItem' instance or static type that this property belongs to . */
        owner: TOwner;

        /** A reference to the static property information for the property instance. */
        staticProperty: StaticProperty<TDataType>; // WARNING: This is null for non-registered "ad-hoc" properties.

        /** The current instance value for the property.
        * Note: You shouldn't read this directly unless you wish to bypass the filters.  Call 'getValue()' instead.
        * If you MUST access this value, you'll have to use the "(<any>property).value" format.
        */
        private value: any;

        valueIsProperty: boolean; // (true if the value is another property reference [updated each time 'setValue()' is called])

        private __timeoutHandle: number;

        /** This property is false until the default value is set for the first time by calling 'reset()'. */
        initialized: boolean = false;

        // -------------------------------------------------------------------------------------------------------------------

        constructor(owner: TOwner, staticProperty: StaticProperty<TDataType>, value: any) {
            super();
            this.owner = owner || null;
            this.staticProperty = staticProperty || null;
            this.value = value;
        }

        // -------------------------------------------------------------------------------------------------------------------

        /** The name of this property (taken from this.staticProperty.name). */
        name(): string { return this.staticProperty ? this.staticProperty.name : UNDEFINED; }

        // -------------------------------------------------------------------------------------------------------------------

        setValue(value: any, triggerChangeEvents: boolean = true): void {
            if (value !== this.value) {
                var ownerIsInitialized = !!this.owner.__saveStates;
                // ... if the owner is initialized, trigger the "changing" event first ...

                if (triggerChangeEvents && ownerIsInitialized) { // (events are never triggered until the initial layout call has been made, since constructors may be setting up properties)
                    if (this.staticProperty && this.staticProperty.interceptors) // (note: ad-hoc properties don't have static info)
                        value = this.staticProperty.__DoOnPropertyChanging(this.owner, this, value);

                    if (this.owner.interceptors)
                        value = this.owner.__DoOnPropertyChanging(this.owner, this, value);

                    if (this.interceptors)
                        value = this.__DoOnPropertyChanging(this.owner, this, value); // (the more local call takes precedence [has the final say])
                }

                // ... set the value ...

                this.value = value;
                this.valueIsProperty = typeof value === 'object' && value instanceof Property;

                // ... call any specialized static client handlers after the value has changed, but BEFORE the "changed" event ...
                // (note: this is provided to support client visual update situations, such as updated HTML element attributes)

                if (this.staticProperty.clientPropertyHandler)
                    this.staticProperty.clientPropertyHandler(this, value);

                // ... if the owner is initialized, trigger the "changed" event ...

                if (triggerChangeEvents && ownerIsInitialized) { // (events are never triggered until the initial layout call has been made, since constructors may be setting up properties)
                    if (this.staticProperty && this.staticProperty.listeners) // (note: ad-hoc properties don't have static info)
                        this.staticProperty.__DoOnPropertyChanged(this.owner, this);

                    if (this.owner.listeners)
                        this.owner.__DoOnPropertyChanged(this.owner, this);

                    if (this.listeners)
                        this.__DoOnPropertyChanged(this.owner, this); // (the more local call takes precedence [has the final say])

                    // ... trigger handlers that wish to know if ANY property has changed ...

                    if (this.owner.__propertyChangedHandlers)
                        this.owner.__DoOnAnyPropertyChanged(this);
                }

                // ... invalidate visuals of a property was updated ...
                if (this.staticProperty && this.staticProperty.isVisual && host.isClient())
                    this.owner['__visualPropertyUpdated'](this);
            }

            this.initialized = true;
        }

        getValue(): any {
            var value = (this.valueIsProperty && this.value !== this) ? (<Property<TOwner, TDataType>>this.value).getValue() : this.value;

            if (this.owner.__saveStates) { // (events are never triggered until the initial layout call has been made, since constructors may be setting up properties)
                if (this.staticProperty && this.staticProperty.filters) // (note: ad-hoc properties don't have static info)
                    value = this.staticProperty.__FilerValue(this.owner, this, value);

                if (this.owner.filters)
                    value = this.owner.__FilerValue(this.owner, this, value);

                if (this.filters)
                    value = this.__FilerValue(this.owner, this, value); // (the more local call takes precedence [has the final say])
            }

            return value;
        }

        /** Returns true if the property value is NOT undefined. */
        hasValue(): boolean { return this.value !== void 0; }

        /** Resets the property to the default value (stored in the registered static property), or 'undefined' if there's no default value. */
        reset(): void {
            this.setValue(this.staticProperty ? this.staticProperty.defaultValue : void 0);
        }

        // -------------------------------------------------------------------------------------------------------------------

        toString(): string { return (this.value || "").toString(); }
        toLocaleString(): string { return (this.value || "").toLocaleString(); }
        valueOf(): any { return this.value; }

        // -------------------------------------------------------------------------------------------------------------------

        /** Creates a deep copy of this graph item property instance via a call to 'DreamSpace.clone()'. */
        clone(): Property<TOwner, TDataType> { return new Property(this.owner, this.staticProperty, Utilities.clone(this.value)); }

        // -------------------------------------------------------------------------------------------------------------------
    }

    // =======================================================================================================================
} // (DreamSpace)

// ###########################################################################################################################
